硬不硬你说了算!近 40 张图解被问千百遍的 TCP 三次握手和四次挥手面试题
The following article is from 小林coding Author 小林coding
前言
TCP
的知识点可以说是的必问的了。TCP
面试题被刷,真是又爱又狠….TCP 基本认识
TCP 连接建立
TCP 连接断开
Socket 编程
PS:本次文章不涉及 TCP 流量控制、拥塞控制、可靠性传输等方面知识,这些留在下篇哈!
正文
01 TCP 基本认识
瞧瞧 TCP 头格式
ACK:该位为
1
时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外该位必须设置为1
。RST:该位为
1
时,表示 TCP 连接中出现异常必须强制断开连接。SYC:该位为
1
时,表示希望建立连,并在其「序列号」的字段进行序列号初始值的设定。FIN:该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位置为 1 的 TCP 段。
为什么需要 TCP 协议?TCP 工作在哪一层?
IP
层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。TCP
协议来负责。什么是 TCP ?
面向连接:一定是「一对一」才能连接,不能像 UDP 协议 可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。
什么是 TCP 连接?
Socket:由 IP 地址和端口号组成
序列号:用来解决乱序问题等
窗口大小:用来做流量控制
如何唯一确定一个 TCP 连接呢?
源地址
源端口
目的地址
目的端口
有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?
2
的 32
次方,客户端的端口数最多为 2
的 16
次方,也就是服务端单机最大 TCP 连接数,约为 2
的 48
次方。首先主要是文件描述符限制,Socket 都是文件,所以首先要通过
ulimit
配置文件描述符的数目;另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统是有限的。
UDP 和 TCP 有什么区别呢?分别的应用场景是?
8
个字节( 64 位),UDP 的头部格式如下:目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。
包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。
校验和:校验和是为了提供可靠的 UDP 首部和数据而设计。
TCP 是面向连接的传输层协议,传输数据前先要建立连接。
UDP 是不需要连接,即刻传输数据。
TCP 是一对一的两点服务,即一条连接只有两个端点。
UDP 支持一对一、一对多、多对多的交互通信
TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
UDP 是尽最大努力交付,不保证可靠交付数据。
TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是
20
个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
FTP
文件传输HTTP
/HTTPS
包总量较少的通信,如
DNS
、SNMP
等视频、音频等多媒体通信
广播通信
为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?
为什么 UDP 头部有「包长度」字段,而 TCP 头部则没有「包长度」字段呢?
4
字节的整数倍。4
字节的整数倍了,所以小林觉得这可能是为了补全 UDP 首部长度是 4
字节的整数倍,才补充了「包长度」字段。02 TCP 连接建立
TCP 三次握手过程和状态变迁
一开始,客户端和服务端都处于
CLOSED
状态。先是服务端主动监听某个端口,处于LISTEN
状态
客户端会随机初始化序号(
client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态。
服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态。
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于ESTABLISHED
状态。服务器收到客户端的应答报文后,也进入
ESTABLISHED
状态。
ESTABLISHED
状态,此致连接就已建立完成,客户端和服务端就可以相互发送数据了。如何在 Linux 系统中查看 TCP 状态?
netstat -napt
命令查看。为什么是三次握手?不是两次、四次?
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
三次握手才可以阻止历史重复连接的初始化(主要原因)
三次握手才可以同步双方的初始序列号
三次握手才可以避免资源浪费
一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
那么此时服务端就会回一个
SYN + ACK
报文给客户端;客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
RST
报文给服务端,表示中止这一次连接。
如果是历史连接(序列号过期或超时),则第三次握手发送的报文是
RST
报文,以此中止历史连接;如果不是历史连接,则第三次发送的报文是
ACK
报文,通信双方就会成功建立连接;
接收方可以去除重复的数据;
接收方可以根据数据包的序列号按序接收;
可以标识发送出去的数据包中, 哪些是已经被对方收到的;
SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。SYN
请求连接在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK
确认信号,所以每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?SYN
阻塞了,重复发送多次 SYN
报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。SYN
报文,而造成重复分配资源。「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
为什么客户端和服务端的初始序列号 ISN 是不相同的?
初始序列号 ISN 是如何随机产生的?
ISN
是基于时钟的,每 4 毫秒 + 1,转一圈要 4.55 个小时。M
是一个计时器,这个计时器每隔 4 毫秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
MTU
:一个网络包的最大长度,以太网中一般为1500
字节;MSS
:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;
MTU
大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP 层来进行重新组装后,在交给上一层 TCP 传输层。什么是 SYN 攻击?如何避免 SYN 攻击?
SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的最大值如下参数:
SYN_RCVD 状态连接的最大个数:
超出处理能时,对新的 SYN 直接回 RST,丢弃连接:
SYN
(未完成连接建立)队列与 Accpet
(已完成连接建立)队列是如何工作的?当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出的连接。
如果应用程序过慢时,就会导致「 Accept 队列」被占满。
如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满。
tcp_syncookies
的方式可以应对 SYN 攻击的方法:当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
计算出一个
cookie
值,再以 SYN + ACK 中的「序列号」返回客户端,服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到「 Accept 队列」。
最后应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出的连接。
03 TCP 连接断开
TCP 四次挥手过程和状态变迁
客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态。服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSED_WAIT
状态。客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态。等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态。客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态服务器收到了
ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭。客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭。
为什么挥手需要四次?
FIN
包的过程,就能理解为什么需要四次了。关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
ACK
和 FIN
一般都会分开发送,从而比三次握手导致多了一次。为什么 TIME_WAIT 等待的时间是 2MSL?
MSL
是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL
字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。2MSL
默认是 60
秒,那么一个 MSL
也就是 30
秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。state, about 60 seconds */
为什么需要 TIME_WAIT 状态?
TIME-WAIT
状态。防止具有相同「四元组」的「旧」数据包被收到;
保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭;
如上图黄色框框服务端在关闭连接之前发送的
SEQ = 301
报文,被网络延迟了。这时有相同端口的 TCP 连接被复用后,被延迟的
SEQ = 301
抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。如上图红色框框客户端四次挥手的最后一个
ACK
报文如果在网络中被丢失了,此时如果客户端TIME-WAIT
过短或没有,则就直接进入了CLOSE
状态了,那么服务端则会一直处在LASE-ACK
状态。当客户端发起建立连接的
SYN
请求报文后,服务端会发送RST
报文给客户端,连接建立的过程就会被终止。
服务端正常收到四次挥手的最后一个
ACK
报文,则服务端正常关闭连接。服务端没有收到四次挥手的最后一个
ACK
报文时,则会重发FIN
关闭连接报文并等待新的ACK
报文。
TIME-WAIT
状态等待 2MSL
时间后,就可以保证双方的连接都可以正常的关闭。TIME_WAIT 过多有什么危害?
第一是内存资源占用;
第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口;
32768~61000
,也可以通过如下参数设置指定如何优化 TIME_WAIT?
打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
net.ipv4.tcp_max_tw_buckets
程序中使用 SO_LINGER ,应用强制使用 RST 关闭。
2MSL
问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。net.ipv4.tcp_tw_reuse
要慎用,因为使用了它就必然要打开时间戳的支持 net.ipv4.tcp_timestamps
,当客户端与服务端主机时间不同步时,客户端的发送的消息会被直接拒绝掉。小林在工作中就遇到过。。。排查了非常的久so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
l_onoff
为非 0, 且l_linger
值为 0,那么调用close
后,会立该发送一个RST
标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT
状态,直接关闭。TIME_WAIT
状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。如果已经建立了连接,但是客户端突然出现故障了怎么办?
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制
tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
03 Socket 编程
针对 TCP 应该如何 Socket 编程?
服务端和客户端初始化
socket
,得到文件描述符;服务端调用
bind
,将绑定在 IP 地址和端口;服务端调用
listen
,进行监听;服务端调用
accept
,等待客户端连接;客户端调用
connect
,向服务器端的地址和端口发起连接请求;服务端
accept
返回用于传输的socket
的文件描述符;客户端调用
write
写入数据;服务端调用read
读取数据;客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。listen 时候参数 backlog 的意义?
未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
参数一 socketfd 为 socketfd 文件描述符
参数二 backlog,这参数在历史有一定的变化
accept 发送在三次握手的哪一步?
客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端进入 SYNC_SENT 状态;
服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对 SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 server_isn,服务器端进入 SYNC_RCVD 状态;
客户端协议栈收到 ACK 之后,使得应用程序从
connect
调用返回,表示客户端到服务器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1;应答包到达服务器端后,服务器端协议栈使得
accept
阻塞调用返回,这个时候服务器端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。
客户端调用 close 了,连接是断开的流程是什么?
close
,会发生什么?客户端调用
close
,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入 FIN_WAIT_1 状态;服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符
EOF
到接收缓冲区中,应用程序可以通过read
调用来感知这个 FIN 包。这个EOF
会被放在已排队等候的其他已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额外数据到达。此时,服务端进入 CLOSE_WAIT 状态;接着,当处理完数据后,自然就会读到
EOF
,于是也调用close
关闭它的套接字,这会使得会发出一个 FIN 包,之后处于 LAST_ACK 状态;客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
客户端进过
2MSL
时间之后,也进入 CLOSED 状态;
参考
推荐阅读
用Python绘制诱人的桑基图,一眼看透熬夜和狗粮的秘密...
Zoom隐私安全问题大爆发,中国背景被聚焦,CEO袁征:过程真的太痛苦了
中文版开源!这或许是最经典的Python编程教程!完整版开放下载
呕心沥血总结的14张思维导图,教你构建 Python核心知识体系(附高清下载)